using Moq;
using NUnit.Framework;

namespace OpenWaves.UnitTests
{
    [TestFixture]
    public class ServiceLocatorTests
    {
        [Test(Description = "When resolver can provide service instance, Resolve returns the instance")]
        public void When_resolver_can_provide_service_instance_Resolve_returns_the_instance()
        {
            var serviceInstance = new Service();

            ServiceLocator.SetResolver(CreateResolverWithServiceInstance(serviceInstance));

            Assert.That(ServiceLocator.Resolve<IService>(), Is.SameAs(serviceInstance));
        }

        [Test]
        public void When_resolver_can_not_provide_service_instance_and_service_has_default_implementation_Resolve_returns_default_implementation()
        {
            ServiceLocator.SetResolver(CreateNullResolver());
            
            Assert.That(ServiceLocator.Resolve<IServiceWithDefaultImplementation>(), Is.InstanceOf<Service>());
        }

        [Test]
        public void When_resolving_default_implementation_Resolve_should_always_return_a_single_instance_of_default_implementation()
        {
            ServiceLocator.SetResolver(CreateNullResolver());
           
            var service = ServiceLocator.Resolve<IServiceWithDefaultImplementation>();

            Assert.That(ServiceLocator.Resolve<IServiceWithDefaultImplementation>(), Is.SameAs(service));
        }

        [Test]
        [ExpectedException]
        public void When_resolver_can_not_provide_service_instance_and_service_has_no_default_implementation_Resolve_throws()
        {
            ServiceLocator.Resolve<IService>();
        }

        [Test]
        public void When_new_resolver_scope_is_created_scoped_resolver_is_used_to_resolve_services()
        {
            var a = new Mock<IService>().Object;
            var b = new Mock<IService>().Object;

            ServiceLocator.SetResolver(CreateResolverWithServiceInstance(a));

            using (ServiceLocator.SetScopedResolver(CreateResolverWithServiceInstance(b)))
            {
                Assert.That(ServiceLocator.Resolve<IService>(), Is.SameAs(b));                
            }
        }

        [Test]
        public void After_leaving_resolver_scope_global_resolver_is_used_to_resolve_services()
        {
            var a = new Mock<IService>().Object;
            var b = new Mock<IService>().Object;

            ServiceLocator.SetResolver(CreateResolverWithServiceInstance(a));

            using (ServiceLocator.SetScopedResolver(CreateResolverWithServiceInstance(b)))
            {
            }

            Assert.That(ServiceLocator.Resolve<IService>(), Is.SameAs(a));
        }

        [Test]
        public void Given_nested_scoped_resolvers_When_current_scoped_resolver_cannot_provide_instance_Then_previous_scoped_resolver_is_queried()
        {
            var a = new Mock<IService>().Object;
            var b = new Mock<IService>().Object;

            ServiceLocator.SetResolver(CreateResolverWithServiceInstance(a));

            using (ServiceLocator.SetScopedResolver(CreateResolverWithServiceInstance(b)))
            {
                using (ServiceLocator.SetScopedResolver(CreateNullResolver()))
                {
                    Assert.That(ServiceLocator.Resolve<IService>(), Is.SameAs(b));
                }
            }
        }

        public interface IService
        {
        }

        [DefaultImplementation(typeof(Service))]
        public interface IServiceWithDefaultImplementation
        {
        }

        public class Service : IService, IServiceWithDefaultImplementation
        {
            public static int InstancesCount = 0;

            public Service()
            {
                InstancesCount++;
            }
        }

        private static Resolver CreateResolverWithServiceInstance(IService serviceInstance)
        {
            return new Resolver(serviceInstance);
        }

        private static IResolver CreateNullResolver()
        {
            var resolveMock = new Mock<IResolver>();
            IService service;
            resolveMock.Setup(r => r.TryResolve(out service)).Returns(false);
            return resolveMock.Object;
        }

        private class Resolver : IResolver
        {
            private readonly IService service;

            public Resolver(IService service)
            {
                this.service = service;
            }

            public bool TryResolve<T>(out T s) where T : class
            {
                if (this.service is T)
                {
                    s = (T) this.service;
                    return true;
                }

                s = null;
                return false;
            }
        }
    }
}